#! /bin/bash

# set the location of some files
awk=/usr/bin/awk
cut=/usr/bin/cut
date=/bin/date
find=/usr/bin/find
fakeroot=/usr/bin/fakeroot
ls=/bin/ls
mkdir=/bin/mkdir
mv=/bin/mv
nl=/usr/bin/nl
perl=/usr/bin/perl
pkgadd=/usr/sbin/pkgadd
pkginfo=/usr/bin/pkginfo
pkgrm=/usr/sbin/pkgrm
sed=/bin/sed
tail=/usr/bin/tail
wc=/usr/bin/wc
xargs=/usr/bin/xargs

date_format="%Y-%m-%d %H:%M:%S"

# overwrite the cd command and exit if changing directory fails
function cd() {
    local directory="$1" retval
    if [ -z "$directory" ]; then
	message -p 3 "${FUNCNAME}(): no directory given"
	exit 1
    fi
    log_command "cd \"$directory\""
    builtin cd "$directory" > /dev/null
    retval=$?
    if [ $retval -ne 0 ]; then
	message -p 3 "${FUNCNAME}(): failed to change to directory \
\"$directory\"" >&2
	exit $retval
    fi
    return 0
}

function get_timestamp() {
    # there is a loadable "strftime" in examples/loadables that we could use
    $date +"$date_format"
}

# expects messages from stdin and prefixes the timestamp
function prefix_timestamp() {
    if [ "$log_with_timestamp" = yes ]; then
	function prefix_timestamp() {
	    local line
	    while read -r line; do
		echo "$(get_timestamp) $line"
	    done
	}
    else
	function prefix_timestamp() {
	    cat
	}
    fi
    prefix_timestamp
}

# writes the given command to a file if $COMMANDFILE is set
function log_command() {
    if [ -n "${COMMANDFILE}" ]; then
	function log_command() {
	    if [ "$1" = eval ]; then
		shift
	    fi
	    echo "$*" >> "${COMMANDFILE}"
	}
    else
	function log_command() {
	    :
	}
    fi
    log_command "$@"
}

function coproc_process() {
    # avoid the use of the pipe (|) which would spawn subshells
    # this way the commands are executed in the current shell
    if [ -n "$LOGFILE" ]; then
	if [ "$quiet" = true ]; then
	    prefix_timestamp >> "$LOGFILE"
	else
	    prefix_timestamp | tee -a "$LOGFILE"
	fi
    else
	if [ "$quiet" = true ]; then
	    cat > /dev/null
	else
	    prefix_timestamp
	fi
    fi
}

# runs a command without changing directory
function _execute() {
    local -a cmd
    local status=0
    cmd=("$@")
    log_command "${cmd[@]}"
    # cannot redirect output of "make" to a program
    # it would only run one target
    case "$(uname)" in
	Interix)
	    # no timestamps on interix
	    "${cmd[@]}"
	    return $?
	    ;;
    esac
    if [ "$NOEXEC" = no ]; then
	# coproc_process() will log stdin
	{ coproc coproc_process >&3 ;} 3>&1
	"${cmd[@]}" >&${COPROC[1]} 2>&1
	status=$?
	eval "exec ${COPROC[1]}>&-"
	wait ${COPROC_PID}
    fi
    return $status
}

# runs any command if $NOEXEC is no
function execute() {
    # cannot do "local -a cmd=("$@")" that gives a syntax error
    local -a cmd
    cmd=("$@")
    message -p 7 "cd \"${workdir}\""
    log_command "cd \"${workdir}\""
    message -p 7  "${cmd[@]}"
    log_command "${cmd[@]}"
    if [ "$NOEXEC" = no ]; then
	cd "${workdir}"
    fi
    _execute "${cmd[@]}"
}

# list all files with a given pattern
function list_files() {
    local pattern="$1" f
    for f in ${pattern}; do
	if [ -f "$f" ]; then
	    echo "$f"
	fi
    done
}

# print on stdout the package instance name of given Package
function get_package_instance() {
    local package="$1"
    if [ -z "$package" ]; then
	message -p 3 "${FUNCNAME}(): parameter <package> missing" >&2
	return 1
    fi
    $pkginfo -d "$package" | $awk '{print $2}'
}

# installs a package in a given directory
function install_package() {
    local package="$1" directory="$2"
    local lockfile=/tmp/.ai.pkg.zone.lock-afdb66cf-1dd1-11b2-a049-000d560ddc3e
    # the following command works under SunOS 5.10
    # if /tmp/.ai.pkg.zone.lock-afdb66cf-1dd1-11b2-a049-000d560ddc3e
    # is writable
#     if [ ! -w "${lockfile}" ]; then
# 	echo "the lockfile ${lockfile} is not writable" >&2
# 	echo "the install command will probably fail" >&2
#     fi
    if [ ! -f "$package" ]; then
	message -p 3 "${FUNCNAME}(): package \"${package}\" is not a file" >&2
	exit 1
    fi
    if [ -z "$directory" ]; then
	message -p 3 "${FUNCNAME}(): missing parameter <directory>" >&2
	return 1
    fi
    # if you add "-n" to the pkgadd comamnd it will run
    # non interactive and exit if interaction would be required
    # As a result we cannot use this option since pkgadd thinks
    # the attributes of directories are different
    eval "echo y | $fakeroot $pkgadd -R \"${directory}\" -d \"${package}\" all"
}

# deinstalls a package from a given directory
function remove_package() {
    local pkginst="$1" directory="$2"
    if [ -z "$pkginst" ]; then
	message -p 3 "${FUNCNAME}(): parameter <pkginst> missing" >&2
	return 1
    fi
    if [ ! -d "$directory" ]; then
	message -p 3 "${FUNCNAME}(): parameter <directory> is not a" \
	    "directory: \"${directory}\""
	return 1
    fi
    $fakeroot $pkgrm -R ${directory} -n ${pkginst}
}

# removes a package from $pkgroot
# the directory given should contain the package that
# has been previously installed
function remove_package_from_dir() {
    local dist_dir="$1" pkginst
    local -a package_file
    package_file=( $(list_files "${dist_dir}/*.pkg") )
    if [ "${#package_file[@]}" -eq 1 ]; then
	# only one package file found, remove it...
	pkginst="$(get_package_instance "${package_file[0]}")"
	remove_package ${pkginst} ${pkgroot}
    elif [ "${#package_file[@]}" -eq 0 ]; then
	message -p 3 "${FUNCNAME}(): no package found in ${dist_dir}" >&2
	return 1
    else
	message -p 3 "${FUNCNAME}(): more than one package found in" \
	"${dist_dir}" >&2
	return 1
    fi
}

# installs a package in $pkgroot that is found in the given
# distribution directory
function install_packages_from_dir() {
    local dist_dir="$1"
    local -a package_files
    package_files=( $(list_files "${dist_dir}/*.pkg") )
    if [ "${#package_files[@]}" -eq 1 ]; then
	# only one package file found, install it...
	install_package "${package_files[0]}" "${pkgroot}"
    elif [ "${#package_files[@]}" -eq 0 ]; then
	message -p 3 "${FUNCNAME}(): no package was generated in" \
	"${dist_dir}" >&2
	return 1
    else
	message -p 3 "${FUNCNAME}(): more than one package found in" \
	"${dist_dir}" >&2
	return 1
    fi
}

# loads a module with the module command
# checks if it was loaded, otherwise exits.
# the module command does not return a proper status
function load_module_or_exit() {
    local module_name="$1"
    message -p 7 "module load $module_name"
    log_command "module load $module_name"
    module load "$module_name"
    # exit status of module() is always 0
    # have to check if it was successful
    if ! module list 2>&1 | grep "$module_name" > /dev/null; then
	message -p 3 "Error while loading module \"$module_name\"" >&2
	exit 1
    fi
}

# removes an element from a list
# expects two arguments:
# 1.   the element to remove
# 2... remaining arguments make up the list
# echoes on stdout the new list
function pop_element() {
    local part="$1" i
    shift
    # 'local -a total=("$@")' does not work
    local -a total
    total=("$@")
    for((i=0; i<${#total[@]}; i++)); do
	if [ "${total[i]}" = "$part" ]; then
	    unset total[i]
	fi
    done
    echo "${total[@]}"
}

# return false if given argument is not an integer
function isint() {
    case "$*" in
	""|*[^0-9]*)
	    return 1
	    ;;
	*)
	    :
    esac
}

# echo given arguments on stdout or appends it to $LOGFILE if defined
# loglevels are:
# 0 emergency
# 1 alert
# 2 critical
# 3 error
# 4 warning
# 5 notice
# 6 info
# 7 debug
function message() {
    local OPTIND=1 LOGLEVEL=6 msg=""
    local -a loglevels
    loglevels=(EMERGENCY ALERT CRITICAL ERROR WARNING NOTICE INFO DEBUG)
    while getopts :p: OPT
    do
	case $OPT in
	    p)
		LOGLEVEL="$OPTARG"
		if ! isint "$LOGLEVEL"; then
		    echo "${FUNCNAME}(): loglevel $LOGLEVEL is no integer" >&2
		    exit 1
		fi
		if [ $LOGLEVEL -gt 7 ]; then
		    echo "${FUNCNAME}(): unknown loglevel $LOGLEVEL" >&2
		    exit 1
		fi
		;;
	    *)
		echo "${FUNCNAME}() invoked with wrong parameter" >&2
		exit 1
	esac
    done
    shift $[ OPTIND - 1 ]

    loglevel=${loglevels[$LOGLEVEL]}
    msg="${SCRIPTNAME} (${loglevel}): ${*}"
    if [ -n "$LOGFILE" ]; then
	if [ "$LOGLEVEL" -le "$VERBOSITY" ]; then
	    if [ "$quiet" = true ]; then
		echo "$msg" | prefix_timestamp >> "${LOGFILE}"
	    else
		echo "$msg" | prefix_timestamp | tee -a "${LOGFILE}"
	    fi
	fi
    else
	# everything lower than WARNING goes to stderr
	if [ $LOGLEVEL -lt 4 ]; then
	    if [ "$LOGLEVEL" -le "$VERBOSITY" ]; then
		if [ "$quiet" != true ]; then
		    echo "$msg" | prefix_timestamp >&2
		fi
	    fi
	else
	    if [ "$LOGLEVEL" -le "$VERBOSITY" ]; then
		if [ "$quiet" != true ]; then
		    echo "$msg" | prefix_timestamp
		fi
	    fi
	fi
    fi
}

# removes /./ and dir/.. from given path
function normpath() {
    local p="$1" _p=""
    until [ "$p" = "$_p" ]; do
	_p="$p"
	p=$(echo "$_p" | $sed -e 's|/\./|/|g' -e 's|[^/]*/\.\./||g')
    done
    echo "$p"
}

# returns absolute path of given argument
function get_absolute_path() {
    local p="$1"
    case "$p" in
	/*)
	    # absolute path, ok
	    :
	    ;;
	*)
	    # relative path, prepend $PWD
	    p="$PWD/${p}"
    esac
    normpath "$p"
}

# return absolute path of collection file
# $COLLECTION_PATH must be defined
function get_collection_path() {
    local collection="$1" dir SAVE_IFS="$IFS"
    case "$collection" in
	/*)
	    # absolute path, ok
	    :
	    ;;
	*)
	    # relative path, search $COLLECTION_PATH
	    IFS=":"
	    for dir in ${COLLECTION_PATH}; do
		collection="${dir}/${collection}"
		if [ -f "${collection}" ]; then
		    break
		fi
	    done
	    IFS="${SAVE_IFS}"
    esac
    normpath "$collection"
}

# expects an item and a list, checks if item is in that list
function is_in_list() {
    local i="$1" l="$2" m retval=1
    for m in $l; do
	if [ "$i" = "$m" ]; then
	    retval=0
	    break
	fi
    done
    return $retval
}

# expects a list of words and echos them in reverse order
function reverse_words() {
    local -a words
    local i revwords=""
    words=($*)
    for((i=${#words[@]}; i>0; i--)); do
	revwords="${revwords} ${words[i-1]}"
    done
    echo $revwords
}

# expects two lists (space separated)
# checks that all members of the first list are in the second list
# echoes the items that are not in the second list
function check_depends() {
    local l1="$1" l2="$2" i1 not_found=""
    for i1 in "$l1"; do
	if ! is_in_list "$i1" "$l2"; then
	    not_found="$not_found $i1"
	fi
    done
    echo $not_found
}

# expects a dependency definition
# prints the action on stdout
function get_action_from_depends() {
    local dep="$1"
    echo "${dep}" | $cut -d: -f1
}

# expects a dependency definition
# print the module name on stdout
function get_module_from_depends() {
    local dep="$1"
    echo "${dep}" | $cut -d: -f2
}

# print the basenames of given files
# but ignore some files
function get_basenames() {
    local b
    for b in "$1"/*; do
	if [ -f "$b" ]; then
	    case "$b" in
		*~)
		    :
		    ;;
		*)
		    echo -n "`basename $b` "
	    esac
	fi
    done
}

# print the files in given directory
function list_collections() {
    local path="$1" files dir SAVE_IFS="$IFS"
    IFS=":"
    for dir in ${path}; do
	echo "${dir}:"
	cd "$dir"
	$find * \( -name .svn -o -name CVS \) -prune -o \
	    -type f \! -name "*~" -print | $xargs $ls
    done
    IFS="${SAVE_IFS}"
}

# substitute the collective_version parameter in the setup script
function subst_collective_version() {
    local setupfile="$1" collective_version="$2"
    $perl -p -i -e "s/^(collective_version=).*/\${1}\"${collective_version}\"/" "${setupfile}"
}

# add a package to the setup script
function add_pkg_to_setup() {
    local pkgfile="$1" setupfile="$2" tag="$3"
    $perl -p -i -e "s/^(# insert ${tag}_pkglist below)/\${1}\n '$pkgfile',/" \
    "$setupfile"
}

# remove duplicate entries from given argument
# the argument is a PATH-variable (colon separated list)
function remove_duplicates_from_path() {
    local path="$1" SAVE_IFS="$IFS" p newpath=""
    IFS=":"
    for p in $path; do
	if [ -z "$newpath" ]; then
	    newpath="$p"
	else
	    if ! is_in_list "$p" "$newpath"; then
		newpath="${newpath}:$p"
	    fi
	fi
    done
    IFS="${SAVE_IFS}"
    echo "$newpath"
}

# exits with status if second argument ($force) is not "yes"
function exit_with_status() {
    local retval="$1" force="$2"
    if [ "$force" != yes ]; then
	exit "$retval"
    fi
}

function run_history_command() {
    local num="$1" history_command
    if ! isint "$num"; then
	message -p 3 "${FUNCNAME}(): argument is not a number: \"$num\"" >&2
	return 2
    fi
    num=$((num * 2))
    history_command=$($sed -n "${num}p" "${histfile}")
    message -p 7 "$history_command"
    eval "${history_command}"
}

# stores the given command line in the history file
# unless the last entry is the same
function save_history() {
    local cmdline="$*" numlines savelines last_line
    if [ ! -d "${histfile%/*}" ]; then
	$mkdir -p "${histfile%/*}"
    fi
    if [ -d "${histfile%/*}" ]; then
	# check if the last line is identical to this
	last_line=$($tail -1 "${histfile}")
	if [ "$last_line" = "$cmdline" ]; then
	    return
	fi
	echo "# `$date`" >> "${histfile}"
	echo "$cmdline" >> "${histfile}"
	# limit the size
	numlines=$($wc -l "${histfile}" | $awk '{print $1}')
	if isint "$histsize"; then
	    if isint "$numlines"; then
		savelines=$((histsize * 2))
		if [ "$numlines" -gt "$savelines" ]; then
		    $tail -$savelines "${histfile}" > "${histfile}".$$
		    $mv "${histfile}".$$ "${histfile}"
		fi
	    fi
	else
	    message -p 3 "Value of \$histsize is not a number" >&2
	fi
    fi

}

# runs "eval <command>" and returns its status
function execute_command() {
    local command="$1" retval
    execute eval "${command}"
    retval=$?
    return $retval
}

# this runs the pre, main, post and test actions if defined
# if the command is not defined then don't run the test
# do not print a warning if pre_ or post_ commands are not defined
# expects: <module> <action>
function run_command_cycle() {
    local modname="$1" action="$2" p retval command command_var run_test=true
    for p in pre_ "" post_ test_; do
	command_var="${p}${action}_command"
	command="$(${parseconfig} ${modname} "${command_var}" \
		    2>/dev/null)"

	if [ -n "$command" ]; then
	    if [ "$p" = test_ ]; then
		if ! $run_test; then
		    continue
		fi
	    fi
	    
	    message -p 7 "Running \"${command_var}\" for" \
		"\"${modname}\":" \
		"${command}"
	    execute_command "${command}"
	    retval=$?
	    if [ $retval -ne 0 ]; then
		message -p 3 "The command \"${command_var}\"" \
		    "\"${command}\"" \
		    "of \"${modname}\" returned error $retval"
		return $retval
	    fi
	else
	    case "$p" in
		pre_|post_)
		    # ignore if not defined
		    :
		    ;;
		"")
		    # command not defined so skip the test
		    run_test=false
		    message -p 4 "Command \"${command_var}\" not defined in" \
			"\"${modname}\"" >&2
		    ;;
		test_)
		    message -p 4 "Command \"${command_var}\" not defined in" \
			"\"${modname}\"" >&2
		    ;;
	    esac
	fi
    done
}

# run all depends commands and exit if one fails
# check for recursion
function run_all_depends() {
    # check if there is a depends option defined
    # that looks like <action>_depends (install_depends, make_depends, ...)
    # If there is then check if the dependency is fulfilled by running
    # the given action:
    # If the package A depends on <action>:<module> then run
    # the <action>_command of <module>
    local modname="$1" action="$2" depends_opt dependencies dependency
    local test_action test_module test_command_var test_command retval
    local dependency_list="$3"
    depends_opt="${action}_depends"
    dependencies=$(${parseconfig} "${modname}" "${depends_opt}" 2>/dev/null)
    if [ -n "$dependencies" ]; then
	# the dependency defines the test-action as well as the module
	# it looks like <action>:<module>
	# then the test in <module> looks like <action>_command
	# get the command from the config file
	for dependency in ${dependencies}; do
	    dependency_list="${dependency_list} ${action}:${modname}"
	    if is_in_list "$dependency" "$dependency_list"; then
		message -p 3 "recursive dependency: $dependency_list"
		exit_with_status 1
	    fi
	    test_action=$(get_action_from_depends "${dependency}")
	    test_module=$(get_module_from_depends "${dependency}")
	    run_all_depends "${test_module}" "${test_action}" \
		"${dependency_list}"
	    run_command_cycle "${test_module}" "${test_action}"
	    retval=$?
	    if [ $retval -ne 0 ]; then
		exit_with_status $retval "$force"
	    fi
	done
    else
	# no more dependency
	return
    fi
}

# return elments of list 1 that are also in list 2
# use the order of list 1
# list is a space separated string
function intersection() {
    local list1="$1" list2="$2" new_list el
    for el in $list1; do
	if is_in_list "$el" "$list2"; then
	    new_list="$new_list $el"
	fi
    done
    echo $new_list
}
